using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using AzureContrib.WindowsAzure.Properties;
using AzureContrib.WindowsAzure.ServiceRuntime;
using AzureContrib.WindowsAzure.Utils;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using RoleEnvironmentException = AzureContrib.WindowsAzure.ServiceRuntime.RoleEnvironmentException;

namespace AzureContrib.WindowsAzure.StorageClient
{
	/// <summary>
	/// An implementation of <see cref="ICloudDriveManager"/> that handles cloud drives.
	/// </summary>
	public class CloudDriveManager : ICloudDriveManager
	{
		/// <summary>
		/// By convention this value is used for a local cache for cloud drives.
		/// </summary>
		public const string DefaultLocalDriveCacheName = "AzureContrib.WindowsAzure.ServiceRuntime.CloudDriveManager.DefaultLocalDriveCacheName";

		private readonly IRoleEnvironment roleEnvironment;
		private readonly string localResourceCacheName;

		private readonly IDictionary<DirectoryInfo, Tuple<CloudDrive, StorageCredentials>> managedDrives;
		private bool cacheInitialized;

		/// <summary>
		/// Ctor
		/// </summary>
		/// <param name="roleEnvironment">A local resource manager</param>
		/// <param name="localResourceCacheName">A name for the local chache with the convention <see cref="DefaultLocalDriveCacheName"/>.</param>
		public CloudDriveManager(IRoleEnvironment roleEnvironment, string localResourceCacheName = DefaultLocalDriveCacheName)
		{
			this.roleEnvironment = roleEnvironment;
			this.localResourceCacheName = localResourceCacheName;

			if (localResourceCacheName == DefaultLocalDriveCacheName)
			{
				Trace.TraceInformation(Strings.LocalDriveCacheNameConventionUsed, DefaultLocalDriveCacheName);
			}

			managedDrives = new Dictionary<DirectoryInfo, Tuple<CloudDrive, StorageCredentials>>();
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.InitializeCache" />
		/// </summary>
		public void InitializeCache(DirectoryInfo cacheRootPath, int totalCacheInMegabytes)
		{
			if (cacheInitialized)
			{
				Trace.TraceInformation(Strings.LocalDriveCacheAlreadyInitialized);
				return;
			}

			Trace.TraceInformation(Strings.LocalDriveCacheInitializing);

			CloudDrive.InitializeCache(cacheRootPath.FullName, totalCacheInMegabytes);
			TotalCacheInMegabytes = totalCacheInMegabytes;
			RemainingCache = totalCacheInMegabytes;

			Trace.TraceInformation(Strings.LocalDriveCacheInitialized, cacheRootPath.FullName, totalCacheInMegabytes);
			cacheInitialized = true;
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.TotalCacheInMegabytes" />
		/// </summary>
		public int TotalCacheInMegabytes { get; private set; }

		/// <summary>
		/// <see cref="ICloudDriveManager.RemainingCache" />
		/// </summary>
		public int RemainingCache { get; private set; }

		/// <summary>
		/// <see cref="ICloudDriveManager.Exists" />
		/// </summary>
		public bool Exists(StorageCredentials storageCredentials, Uri driveAddress)
		{
			var cloudBlob = new CloudBlob(driveAddress.AbsoluteUri, storageCredentials);

			if (!cloudBlob.Container.Exists() || !cloudBlob.Exists()) return false;

			if (cloudBlob.Attributes.Properties.BlobType != BlobType.PageBlob)
			{
				var message = string.Format(Strings.BlobInAccountDoesNotExist, driveAddress.AbsoluteUri, storageCredentials.AccountName);
				Trace.TraceError(message);
				throw new CloudDriveManagerException(message);
			}

			return true;
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.Create" />
		/// </summary>
		public void Create(StorageCredentials storageCredentials, Uri driveAddress, int sizeInMb)
		{
			if (Exists(storageCredentials, driveAddress))
			{
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					var message = string.Format(Strings.BlobExistsCannotCreate, storageCredentials.AccountName, driveAddress.AbsoluteUri);
					Trace.TraceError(message);
					throw new CloudDriveManagerException(message);
				}
			}

			var cloudDrive = new CloudDrive(driveAddress, storageCredentials);

			if (sizeInMb < CloudDriveSize.MinValue || sizeInMb > CloudDriveSize.MaxValue)
			{
				// Data makes no sense.
				Trace.TraceWarning(Strings.CloudDriveSizeOutOfRange, sizeInMb);
				return;
			}

			Trace.TraceInformation(Strings.CloudDriveBlobCreating, cloudDrive.Uri.AbsoluteUri);

			try
			{
				cloudDrive.Create(sizeInMb);
				Trace.TraceInformation(Strings.CloudDriveBlobCreated, cloudDrive.Uri.AbsoluteUri);
			}
			catch (CloudDriveException cde)
			{
				// This is the compute emulator windows azure drive special case:
				// On local disk drives are not stored in blob storage and any cloudBlob.Exists() call will allways fail.
				// This means this exception is very likely OK. Just proceed as if nothing happened for DevelopmentStorageAccount.
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					var message = CreateAndTraceExceptionMessage(cde, string.Format(Strings.CloudDriveBlobUnableToCreate, cloudDrive.Uri.AbsoluteUri));
					throw new CloudDriveManagerException(message, cde);
				}
			}
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.Mount" />
		/// </summary>
		public DirectoryInfo Mount(StorageCredentials storageCredentials, Uri driveAddress, int cacheSize = 0, DriveMountOptions driveMountOptions = DriveMountOptions.Force)
		{
			if (!Exists(storageCredentials, driveAddress))
			{
				if (!storageCredentials.AreDevelopmentStorageCredentials())
				{
					var message = string.Format(Strings.CannotFindCloudDriveBlobAtAccountAndLocation, storageCredentials.AccountName, driveAddress.AbsoluteUri);
					Trace.TraceError(message);
					throw new CloudDriveManagerException(message);
				}
			}

			if (cacheSize < 0)
			{
				Trace.TraceWarning(Strings.CloudDriveSizeOutOfRange);
				cacheSize = 0;
			}

			if (cacheSize > RemainingCache)
			{
				Trace.TraceWarning(Strings.CloudDriveCacheRequestTooBig, cacheSize, RemainingCache);
				cacheSize = RemainingCache;
			}

			WarmUpCache();

			var cloudDrive = new CloudDrive(driveAddress, storageCredentials);

			Trace.TraceInformation(Strings.MountingDriveForBlobOnAccount, cloudDrive.Uri.AbsolutePath, cloudDrive.Credentials.AccountName);

			var msDriveMountOptions = driveMountOptions.ToEnum<Microsoft.WindowsAzure.StorageClient.DriveMountOptions>();

			string driveRootPath;

			try
			{
				driveRootPath = cloudDrive.Mount(cacheSize, msDriveMountOptions);
				RemainingCache = RemainingCache - cacheSize;

				Trace.TraceInformation(Strings.DriveMountedForBlobToLocalPath, cloudDrive.Uri.AbsolutePath, driveRootPath);
			}
			catch (CloudDriveException cde)
			{
				var message = CreateAndTraceExceptionMessage(cde, string.Format(Strings.CloudDriveBlobUnableToMount, cloudDrive.Uri.AbsoluteUri));
				Trace.TraceInformation(message);
				throw new CloudDriveManagerException(message, cde);
			}

			managedDrives.Add(new DirectoryInfo(cloudDrive.LocalPath), Tuple.Create(cloudDrive, storageCredentials));

			return new DirectoryInfo(driveRootPath);
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.Unmount" />
		/// </summary>
		public void Unmount(DirectoryInfo driveRootDirectory)
		{
			CloudDrive cloudDrive;

			if (!TryGetDrive(driveRootDirectory, out cloudDrive)) return;

			lock (driveRootDirectory)
			{
				WarmUpCache();

				if (IsMounted(cloudDrive))
				{
					Trace.TraceInformation(Strings.UnmountingCloudDriveBlobDetailed, cloudDrive.Credentials.AccountName, cloudDrive.Uri.AbsoluteUri, cloudDrive.LocalPath);

					cloudDrive.Unmount();
				}
				managedDrives.Remove(driveRootDirectory);

				Trace.TraceInformation(Strings.UnmountedCloudDriveBlobDetailed, cloudDrive.Credentials.AccountName, cloudDrive.Uri.AbsoluteUri, cloudDrive.LocalPath);
			}
		}

		/// <summary>
		/// <see cref="ICloudDriveManager.Delete" />
		/// </summary>
		public void Delete(DirectoryInfo driveRootDirectory)
		{
			CloudDrive cloudDrive;

			if (!TryGetDrive(driveRootDirectory, out cloudDrive)) return;

			lock (driveRootDirectory)
			{
				WarmUpCache();

				if (IsMounted(cloudDrive))
				{
					Trace.TraceInformation(Strings.DeletingManagedDriveAtDirectory, driveRootDirectory.FullName);
					cloudDrive.Delete();
				}
				else
				{
					Trace.TraceInformation(Strings.ManagedDriveAtLocationNotMounted, driveRootDirectory.FullName);
				}
				managedDrives.Remove(driveRootDirectory);
			}
		}

		/// <summary>
		/// Iterate over all managed drives
		/// </summary>
		/// <returns>DirectoryInfos to all of the managed drives root paths.</returns>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		/// <summary>
		/// Iterate over all managed drives
		/// </summary>
		/// <returns>DirectoryInfos to all of the managed drives root paths.</returns>
		/// <remarks>If a mounted drive was not created through this instance of the CloudDriveManager the StorageCredentials will not be available and will be null.</remarks>
		public IEnumerator<Tuple<DirectoryInfo, Uri, StorageCredentials>> GetEnumerator()
		{
			WarmUpCache();

			return CloudDrive.GetMountedDrives().Select(CreateGetEnumeratorItem).GetEnumerator();
		}

		private Tuple<DirectoryInfo, Uri, StorageCredentials> CreateGetEnumeratorItem(KeyValuePair<string, Uri> item)
		{
			var rootDirectory = new DirectoryInfo(item.Key);
			var uri = item.Value;
			var storageCredentials = managedDrives.ContainsKey(rootDirectory) ? managedDrives[rootDirectory].Item1.Credentials : null;
			return Tuple.Create(rootDirectory, uri, storageCredentials);
		}

		private bool TryGetDrive(DirectoryInfo driveRootDirectory, out CloudDrive cloudDrive)
		{
			Tuple<CloudDrive, StorageCredentials> tuple;
			if (managedDrives.TryGetValue(driveRootDirectory, out tuple))
			{
				cloudDrive = tuple.Item1;
			}
			else
			{
				Trace.TraceInformation(Strings.CloudDriveManagerNotManagingTheDrive, GetType().FullName, driveRootDirectory.FullName);
				cloudDrive = null;
			}

			return cloudDrive != null;
		}

		private void WarmUpCache()
		{
			if (cacheInitialized) return;

			Trace.TraceInformation(Strings.FetchingLocalResourceForDrive, localResourceCacheName);

			ILocalResource localResource;
			try
			{
				localResource = roleEnvironment.GetLocalResource(localResourceCacheName);
				Trace.TraceInformation(Strings.FoundLocalResourceForPath, localResource.Name, localResource.RootPath, localResource.MaximumSizeInMegabytes);
			}
			catch (RoleEnvironmentException rex)
			{
				var message = CreateAndTraceExceptionMessage(rex, string.Format(Strings.UnableToGetLocalResourceNamed, localResourceCacheName));
				throw new CloudDriveManagerException(message, rex);
			}

			InitializeCache(localResource.RootPath, localResource.MaximumSizeInMegabytes);
		}

		private static bool IsMounted(CloudDrive cloudDrive)
		{
			return CloudDrive.GetMountedDrives().Any(keyValuePair => keyValuePair.Key == cloudDrive.LocalPath);
		}

		private static string CreateAndTraceExceptionMessage(Exception e, string baseMessage)
		{
			var message = new StringBuilder(baseMessage);
			message.AppendFormat(Strings.ErrorMessage, e.Message);
			message.AppendFormat(Strings.StackTrace, e.StackTrace);
			var exceptionMessage = message.ToString();
			Trace.TraceError(exceptionMessage);
			return exceptionMessage;
		}
	}
}